<?php
// +-------------------------------------------------------------------------+
// | PHP Version 5                                                           |
// +-------------------------------------------------------------------------+
// | ImaEngine - web development framework                                   |
// | Copyright (c) 2006-2007 Sardar Yumatov <ja.doma@gmail.com>              |
// +-------------------------------------------------------------------------+
// | This library is free software; you can redistribute it and/or modify it |
// | under the terms of the GNU Lesser General Public License as published   |
// | by the Free Software Foundation; either version 2.1 of the License, or  |
// | (at your option) any later version.                                     |
// |                                                                         |
// | This library is distributed in the hope that it will be useful, but     |
// | WITHOUT ANY WARRANTY; without even the implied warranty of              |
// | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.                    |
// | See the GNU Lesser General Public License for more details.             |
// |                                                                         |
// | You should have received a copy of the GNU Lesser General Public        |
// | License along with this library; if not, write to the Free Software     |
// | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA |
// +-------------------------------------------------------------------------+

/**
* Regexp based template parser.
*
* @author Sardar Yumatov <ja.doma@gmail.com>
* @copyright Copyright &copy; 2006-2007 Sardar Yumatov
* @version v1.0
* @package common.template
* @category Parser
*
* @PHP5only
*/

if(!defined('PROCESS')) {
	header('HTTP/1.1 404 Not Found');
	die('NotFound');
}

require_once('lib/GenericExceptions.php');

/**
 * Interface used by the parser to collect all parsed structures.
 * Only tags and text content are recognized.
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
interface TemplateParserHandler {

	/**
	 * Process given tag.
	 * The tag will be processed in:
	 *  - $action name -- the tag action name, by default 'marker'
	 *  - $markers -- list of markers in original order, associative keys preserved
	 *  - $filters -- list of (filter_name => arguments | null) the filters in original order
	 *
	 * Iif method returns false, then parsing will be stopped. Usually you don't need to stop the parser.
	 *
	 * @param string $action the action name (mark, include, block etc)
	 * @param array $markers the list of markers defined by the user
	 * @param array $filters the filter chain with arguments (name => arguments) or null if none found
	 * @param integer $startpos the position in the text at which tag starts
	 * @param integer $endpos the position in the text at the next character after the tag
	 * @return boolean false to stop the parser, true to parse next structure
	 */
	public function processTag($action, $markers, $filters, $startpos, $endpos);

	/**
	 * Collect text data.
	 * @param string $content the template text data
	 * @param integer $startpos the position in the text at which text content starts
	 * @param integer $endpos the position in the text at the next character after the text content
	 * @return void
	 */
	public function textContent($content, $startpos, $endpos);

	/**
	 * Stops processing the template.
	 * This method will be called if parser stops processing the template either because end of the template or
	 * because the handler has stoped the parser.
	 * @param integer $last_offset the last offset where the parser stops, parsing should be start from this offset if needed
	 */
	public function endProcessing($last_offset);
}

/**
 * Template parser.
 *
 * This class parses all tags in given template text.
 * The tags have predefined syntax:  <code> '{%' ( {action} : )? {params} ( | {filter} )* '%}' </code>
 *   - action := ([^\W:|]|[.\-_])+  -- any word character including '.' '-' and '_'
 *   - token := ([^\s|%}{'"=]+)|("([^\\"]|(\\.))*")|('([^\\']|(\\.))*') -- word; "some string"; 'another kind of %} stri\'ng'
 *   - param := {token}(\s*=\s*{token})? -- "value", name="value", 'some name'=some_value
 *   - params := {param}(\s+{param})* -- space separated list of parameters
 * 	 - filter := {action}(\s*:\s*{params})? -- filter_name; filter_name: argument argument2

 * Warning: alttough the identifiers are not restricted, the actual actions may apply additional
 * restriction. For example 'include' action will restrict allowed characters for safety reasons.
 *
 * If action is omitted, then 'marker' action will be used.
 *
 * Examples:
 * 	  {%test%} - marker test, action is by default 'marker'
 *    {%include: my_template %} - with action
 *    {%block: 'some name' | uppercase | iconv: CP1251 out=UTF-8 %} - full syntax
 *
 * Specials sequence '{%%}' outputs '{%'.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
class TemplateParser {

	/** Global template tag
	 * @var string */
	private static $tagreg = null;

	/** Filer parsing reg
	 * @var string */
	private static $filterreg = null;

	/** Parameter list parsing reg
	 * @var string */
	private static $paramreg = null;

	/** String tokens reg
	 * @var string */
	private static $tokensreg = null;

	/** Assemble all regular expressions. */
	private static function init() {
		if(!is_null(self::$tagreg)) return;

		$label = '(?:(?:[^\\W:|]|[.\\-_])+)';
		$token = '(?:(?:[^\\s|%}{\'"=:]+)|(?:"(?:[^\\\\"]|(?:\\\\.))*")|(?:\'(?:[^\\\\\']|(?:\\\\.))*\'))';
		$param = "(?:{$token}(?:\\s*=\\s*{$token})?)";
		$params = "(?:{$param}(?:\\s+{$param})*)";
		$filter = "(?:{$label}(?:\\s*:\\s*{$params})?)";
		$filters = "(?:\\s*\|\\s*{$filter})*";

		self::$tagreg = "/\\{%(?:\\s*(?:({$label})\\s*:\\s*)?({$params})({$filters})\\s*)?%\\}/sSu"; //s - dot matches everything, S - deep analyze, u - utf-8 pattern
		self::$filterreg = "/({$label})(?:\\s*:\\s*({$params}))?/sSu";
		self::$paramreg = "/({$token})(?:\\s*=\\s*({$token}))?/sSu";
		self::$tokensreg = '/([^\\s|%}{\'"=:]+)|(?:"((?:[^\\\\"]|(?:\\\\.))*)")|(?:\'((?:[^\\\\\']|(?:\\\\.))*)\')/sSu';
	}

	/**
	 * Ensure given text will be never parsed as template.
	 * This method ensures no tags will be left as is, all '{%' will be qouted safely.
	 * @param string $str string to parse, then it may be used safely in a template
	 * @return string parsed string, where all '{%' will be replaced by '{%%}'
	 */
	public static function quoteTemplateDocument($str) {
		return str_replace('{%', '{%%}', $str);
	}

	/**
	 * Parse block.
	 * The $handler controlls the parsing process when each tag is found.
	 * @throws SyntaxException on wrong tag syntax (unescaped non tag {% sequence)
	 * @param TemplateParserHandler $handler the parser handler, receives the events and data
	 * @param string $text template source text
	 * @param integer $offset where to start in the source text
	 * @return integer the current parser position, will not be equal to strlen($text) if $handler stops the parsing
	 */
	public static function parse(TemplateParserHandler $handler, $text, $offset = 0) {
		self::init();
		if($offset > ($len = strlen($text))) throw new InvalidArgumentException("Parsing position '{$offset}' is greater than the parsed text '{$len}'!");
		$mth = null;
		while($offset < $len && ($pos = strpos($text, '{%', $offset)) !== false) { //do not ignore wrong {% for better debugging
			$handler->textContent(substr($text, $offset, $pos - $offset), $offset, $pos);
			if(!preg_match(self::$tagreg, $text, $mth, PREG_OFFSET_CAPTURE, $pos) || $mth[0][1] != $pos) throw new SyntaxException("Tag start found at position {$pos}, but further contents are not of correct tag syntax! Near: ".substr($text, $pos, 40));
			$offset = $mth[0][1] + strlen($mth[0][0]);
			if($mth[0][0] == '{%%}') { //special sequence
				$handler->textContent('{%', $mth[0][1], $offset);
			} else { //process a tag
				$action = (!empty($mth[1]) && !empty($mth[1][0]))? $mth[1][0]: 'marker';
				$markers = self::parseParams($mth[2][0]);
				$filters = (!empty($mth[3]) && !empty($mth[3][0]))? self::parseFilters($mth[3][0]): null;
				if(!$handler->processTag($action, $markers, $filters, $mth[0][1], $offset)) {
					$handler->endProcessing($offset);
					return $offset;
				}
			}
		}
		if($offset < $len) $handler->textContent(substr($text, $offset, $len - $offset), $offset, $len);
		$handler->endProcessing($len);
		return $len;
	}

	/**
	 * Parse tag parameters into array.
	 * @param string $text tag chunk to parse
	 * @return array of string paramters in original order with associative keys preserved
	 */
	private static function parseParams($text) {
		$mth = null; $offset = 0; $len = strlen($text); $ret = array(); $i=0;
		while($offset < $len && preg_match(self::$paramreg, $text, $mth, PREG_OFFSET_CAPTURE, $offset)) {
			$offset = $mth[0][1] + strlen($mth[0][0]);
			if(isset($mth[2]) && $mth[2][1] >= 0) $ret[self::parseToken($mth[1][0])] = self::parseToken($mth[2][0]);
			else $ret[$i++] = self::parseToken($mth[1][0]);
		}
		return $ret;
	}

	/**
	 * Parse a token, resolve all escape sequences.
	 * @param string $text tag chunk to parse
	 * @return string token contents
	 */
	private static function parseToken($text) {
		$mth = null;
		preg_match(self::$tokensreg, $text, $mth, PREG_OFFSET_CAPTURE);
		if(isset($mth[1]) && $mth[1][1] >=0) return $mth[1][0];
		else return preg_replace('/\\\\(.)/su', '\\1', (isset($mth[2]) && $mth[2][1] >= 0)? $mth[2][0]: $mth[3][0]);
	}

	/**
	 * Parse filter chain
	 * @param string $text filters chain
	 * @return array parsed filters
	 */
	private static function parseFilters($text) {
		$mth = null; $offset = 0; $len = strlen($text); $ret = array();
		while($offset < $len && preg_match(self::$filterreg, $text, $mth, PREG_OFFSET_CAPTURE, $offset)) {
			$offset = $mth[0][1] + strlen($mth[0][0]);
			if(isset($mth[2]) && $mth[2][1] >= 0) $ret[$mth[1][0]] = self::parseParams($mth[2][0]);
			else $ret[$mth[1][0]] = null;
		}
		return $ret;
	}
}

/**
 * Default parser handler that understands blocks and tags, suitable for most applicatons.
 *
 * This class builds block tree structure, where the root block has all other blocks as plain list. Other
 * blocks will not have children, however the relation to the parent will be preserved. This confirms
 * the original design of <tt>AbstractTemplateDocument</tt> and is easier to navigate to a particular block.
 *
 * <b>Note:</b> usually you don't need to use this class, it is for <tt>*TemplateDocument</tt> developers.
 * Use a concrete implementation of template document (templater) instead of working with the parser.
 *
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
class DefaultTemplateParserHandler implements TemplateParserHandler {

	/**
	 * All block names, they should be unique within a single template.
	 * Just to enforce/simplify the use of templater.
	 * @var array of name => DefaultTemplateParserHandlerBlock
	 */
	private $blocks;

	/**
	 * The global root template block.
	 * @var DefaultTemplateParserHandlerBlock
	 */
	private $rootblock;

	/** Current processed block.
	 * @var DefaultTemplateParserHandlerBlock */
	private $currentBlock;

	/**
	 * Stack of all opened blocks.
	 * @var array
	 */
	private $blockstack;

	/**
	 * Create new handler, that will translate the template to an object tree structure.
	 */
	public function __construct() {
		$this->rootblock = new DefaultTemplateParserHandlerBlock(null, null, null);
		$this->currentBlock = $this->rootblock;
		$this->blockstack = array();
		$this->blocks = array();
	}

	/**
	 * Returns all defined blocks as <tt>name => DefaultTemplateParserHandlerBlock</tt> list.
	 * The root block will not be listed here
	 * @return array
	 */
	public function getBlocks() {
		return $this->blocks;
	}

	/**
	 * Returns the root template block, created when this handler was created.
	 * @return DefaultTemplateParserHandlerBlock
	 */
	public function getRootBlock() {
		return $this->rootblock;
	}

	/**
	 * Returns the block currently being processed.
	 * @return DefaultTemplateParserHandlerBlock
	 */
	public function getCurrentBlock() {
		return $this->currentBlock;
	}

	/**
	 * Returns the stack of all currently opened blocks.
	 * The root block is not in the stack, only really opened blocks will be listed here.
	 * @return array of DefaultTemplateParserHandlerBlock
	 */
	public function getBlockStack() {
		return $this->blockstack;
	}

	/**
	 * Process given tag (instantiate handler).
	 *
	 * This method supports blocks, that are defined by following <tt>$action</tt>'s:
 	 *   - 'block-start' -- case-insensitive, starts block definition. Name must be a single word. Block may have filters.
     *   - 'block-end' -- case-insensitive, ends block definition. Name must be a single word. Filters will be ignored.
     *
     * This means the actions mentioned above will be never propagated to the end template handler.
     * Other tags will be assumed to be 'normal' tags.
     *
     * Block end must have the block name currently being closed for better syntax and simpler debugging. If template tries
 	 * to close non-current block (not from top of the block stack), then exception will be thrown.
 	 * Example: {%block-start: test | some filter %}some content{%block-end: test%}
	 *
	 * @throws SyntaxException on block name dublicate or block stack error
	 * @param string $action the action name (mark, include, block etc)
	 * @param array $markers the list of markers defined by the user
	 * @param array $filters the filter chain with arguments (name => arguments) or null if none found
	 * @param integer $startpos the position in the text at which tag starts
	 * @param integer $endpos the position in the text at the next character after the tag
	 * @return boolean false to stop the parser, true to parse next structure
	 */
	public function processTag($action, $markers, $filters, $startpos, $endpos) {
		if(strcasecmp($action, 'block-start') == 0) {
			if(sizeof($markers) != 1 || !isset($markers[0])) {
				$name = isset($markers[0])? $markers[0]: array_shift($markers);
				throw new SyntaxException("Block name '{$name}' at position: {$startpos} - {$endpos} contains multiple markers, this prevents passing other parameters than blockname! Quote your block name!");
			}
			if(isset($this->blocks[$name = $markers[0]])) throw new SyntaxException("Block with name '{$name}' is already defined in this template! Check the tag at position: {$startpos} - {$endpos}.");
			$this->blockstack[] = $name;
			$this->blocks[$name] = new DefaultTemplateParserHandlerBlock($name, $filters, $this->currentBlock);
			$this->currentBlock = $this->blocks[$name];
		} elseif(strcasecmp($action, 'block-end') == 0) {
			if(sizeof($markers) != 1 || !isset($markers[0])) {
				$name = isset($markers[0])? $markers[0]: array_shift($markers);
				throw new SyntaxException("Block name '{$name}' at position: {$startpos} - {$endpos} contains multiple markers, this prevents passing other parameters than blockname! Quote your block name!");
			}
			if(sizeof($this->blockstack) == 0) throw new SyntaxException("Can't close block {$markers[0]}, no block is opened! Close tag position: {$startpos} - {$endpos}.");
			if($markers[0] != ($top = array_pop($this->blockstack))) throw new SyntaxException("Block {$markers[0]} is closed, while '{$top}' is expected! Close tag position: {$startpos} - {$endpos}.");
			$this->currentBlock = $this->blocks[$top]->getParentBlock();
		} else { //emmit tag
			$this->currentBlock->appendBody(array(
				'action' => $action,
				'markers' => $markers,
				'filters' => $filters,
				'start-offset' => $startpos,
				'end-offset' => $endpos
			));
		}
		return true;
	}

	/**
	 * Append the text data to the current processing block.
	 * The current block is the top most one from the stack.
	 * @param string $content the template text data
	 * @param integer $startpos the position in the text at which text content starts
	 * @param integer $endpos the position in the text at the next character after the text content
	 * @return void
	 */
	public function textContent($content, $startpos, $endpos) {
		$this->currentBlock->appendBody($content);
	}

	/**
	 * Stops processing the template.
	 * This method checks if there are opened blocks. If there is any opened block, then exception will
	 * be throw.
	 * @throws SyntaxException if there are opened blocks
	 * @param integer $last_offset the last offset where the parser stops, parsing should be start from this offset if needed
	 */
	public function endProcessing($last_offset) {
		if(sizeof($this->blockstack) != 0) throw new SyntaxException("There are ".sizeof($this->blockstack)." opened blocks! Lasft offset: {$last_offset}");
	}
}

/**
 * Template block capture implementation.
 * The purpose of this class to hold the text data and all parsed tags.
 * @author Sardar Yumatov <ja.doma@gmail.com>
 */
class DefaultTemplateParserHandlerBlock {

	/** The parend block
	 * @var DefaultTemplateParserHandlerBlock */
	private $parent;

	/** The name of this block, null for root block.
	 * @var string */
	private $name;

	/** List with filters (name => arguments | null)
	 * @var array */
	private $filters;

	/** List of text data and parsed tags (template body)
	 * @var array */
	private $body;

	/**
	 * Construct new empty block.
	 * @param string $name name of the block, null for root block
	 * @param array $filters list of predefined filters, null if not used
	 * @param DefaultTemplateParserHandlerBlock parent of this block, null if this is the root block
	 */
	public function __construct($name, $filters, $parent) {
		$this->name = $name;
		$this->filters = $filters;
		$this->parent = $parent;
	}

	/**
	 * Returns name of this block, null if this is a root block.
	 * @return string
	 */
	public function getName() {
		return $this->name;
	}

	/**
	 * Returns all defined filters of this block, null if nothin was defined.
	 * @return array
	 */
	public function getFilters() {
		return $this->filters;
	}

	/**
	 * Returns parent block, null if this is the root block.
	 * @return DefaultTemplateParserHandlerBlock
	 */
	public function getParentBlock() {
		return $this->parent;
	}

	/**
	 * Returns compiled template body.
	 * Returned list will contain chunks of the template text together with parsed tags, represented
	 * by an associative array with keys:
	 *   - 'action' - tag action
	 *   - 'markers' - list of the tag markers
	 *   - 'filters' - list of the tag filters or null if not defined
	 *   - 'start-offset' - the offset in the original template where tag starts
	 *   - 'end-offset' - the offset in the original template where tag ends
	 *
	 * @return array
	 */
	public function getBody() {
		return $this->body;
	}

	/**
	 * Append new $entry to the body.
	 * The $entry may be a text or an associative array containing all parsed tag data.
	 * @param string|array $entry the template entry, either text or a tag
	 */
	public function appendBody($entry) {
		$this->body[] = $entry;
	}
}
?>